From 6e43be5c7b99dbee49dc72b6f989f29fdd7e9356 Mon Sep 17 00:00:00 2001
From: Matt Johnston <matt@ucc.asn.au>
Date: Mon, 20 Nov 2023 14:02:47 +0800
Subject: [PATCH] Implement Strict KEX mode

As specified by OpenSSH with kex-strict-c-v00@openssh.com and
kex-strict-s-v00@openssh.com.

Upstream: https://github.com/mkj/dropbear/commit/6e43be5c7b99dbee49dc72b6f989f29fdd7e9356
Signed-off-by: Fabrice Fontaine <fontaine.fabrice@gmail.com>
---
 src/cli-session.c    | 11 +++++++++++
 src/common-algo.c    |  6 ++++++
 src/common-kex.c     | 26 +++++++++++++++++++++++++-
 src/kex.h            |  3 +++
 src/process-packet.c | 34 +++++++++++++++++++---------------
 src/ssh.h            |  4 ++++
 src/svr-session.c    |  3 +++
 7 files changed, 71 insertions(+), 16 deletions(-)

diff --git a/cli-session.c b/cli-session.c
index 5981b2470..d261c8f82 100644
--- a/cli-session.c
+++ b/cli-session.c
@@ -46,6 +46,7 @@ static void cli_finished(void) ATTRIB_NORETURN;
 static void recv_msg_service_accept(void);
 static void cli_session_cleanup(void);
 static void recv_msg_global_request_cli(void);
+static void cli_algos_initialise(void);
 
 struct clientsession cli_ses; /* GLOBAL */
 
@@ -117,6 +118,7 @@ void cli_session(int sock_in, int sock_out, struct dropbear_progress_connection
 	}
 
 	chaninitialise(cli_chantypes);
+	cli_algos_initialise();
 
 	/* Set up cli_ses vars */
 	cli_session_init(proxy_cmd_pid);
@@ -487,3 +489,12 @@ void cli_dropbear_log(int priority, const char* format, va_list param) {
 	fflush(stderr);
 }
 
+static void cli_algos_initialise(void) {
+	algo_type *algo;
+	for (algo = sshkex; algo->name; algo++) {
+		if (strcmp(algo->name, SSH_STRICT_KEX_S) == 0) {
+			algo->usable = 0;
+		}
+	}
+}
+
diff --git a/common-algo.c b/common-algo.c
index 378f0ca8e..f9d46ebb6 100644
--- a/common-algo.c
+++ b/common-algo.c
@@ -307,6 +307,12 @@ algo_type sshkex[] = {
 	/* Set unusable by svr_algos_initialise() */
 	{SSH_EXT_INFO_C, 0, NULL, 1, NULL},
 #endif
+#endif
+#if DROPBEAR_CLIENT
+	{SSH_STRICT_KEX_C, 0, NULL, 1, NULL},
+#endif
+#if DROPBEAR_SERVER
+	{SSH_STRICT_KEX_S, 0, NULL, 1, NULL},
 #endif
 	{NULL, 0, NULL, 0, NULL}
 };
diff --git a/common-kex.c b/common-kex.c
index ac8844246..8e33b12a6 100644
--- a/common-kex.c
+++ b/common-kex.c
@@ -183,6 +183,10 @@ void send_msg_newkeys() {
 	gen_new_keys();
 	switch_keys();
 
+	if (ses.kexstate.strict_kex) {
+		ses.transseq = 0;
+	}
+
 	TRACE(("leave send_msg_newkeys"))
 }
 
@@ -193,7 +197,11 @@ void recv_msg_newkeys() {
 
 	ses.kexstate.recvnewkeys = 1;
 	switch_keys();
-	
+
+	if (ses.kexstate.strict_kex) {
+		ses.recvseq = 0;
+	}
+
 	TRACE(("leave recv_msg_newkeys"))
 }
 
@@ -550,6 +558,10 @@ void recv_msg_kexinit() {
 
 	ses.kexstate.recvkexinit = 1;
 
+	if (ses.kexstate.strict_kex && !ses.kexstate.donefirstkex && ses.recvseq != 1) {
+		dropbear_exit("First packet wasn't kexinit");
+	}
+
 	TRACE(("leave recv_msg_kexinit"))
 }
 
@@ -859,6 +871,18 @@ static void read_kex_algos() {
 	}
 #endif
 
+	if (!ses.kexstate.donefirstkex) {
+		const char* strict_name;
+		if (IS_DROPBEAR_CLIENT) {
+			strict_name = SSH_STRICT_KEX_S;
+		} else {
+			strict_name = SSH_STRICT_KEX_C;
+		}
+		if (buf_has_algo(ses.payload, strict_name) == DROPBEAR_SUCCESS) {
+			ses.kexstate.strict_kex = 1;
+		}
+	}
+
 	algo = buf_match_algo(ses.payload, sshkex, kexguess2, &goodguess);
 	allgood &= goodguess;
 	if (algo == NULL || algo->data == NULL) {
diff --git a/kex.h b/kex.h
index 77cf21a37..7fcc3c252 100644
--- a/kex.h
+++ b/kex.h
@@ -83,6 +83,9 @@ struct KEXState {
 
 	unsigned our_first_follows_matches : 1;
 
+	/* Boolean indicating that strict kex mode is in use */
+	unsigned int strict_kex;
+
 	time_t lastkextime; /* time of the last kex */
 	unsigned int datatrans; /* data transmitted since last kex */
 	unsigned int datarecv; /* data received since last kex */
diff --git a/process-packet.c b/process-packet.c
index 945416023..133a152d0 100644
--- a/process-packet.c
+++ b/process-packet.c
@@ -44,6 +44,7 @@ void process_packet() {
 
 	unsigned char type;
 	unsigned int i;
+	unsigned int first_strict_kex = ses.kexstate.strict_kex && !ses.kexstate.donefirstkex;
 	time_t now;
 
 	TRACE2(("enter process_packet"))
@@ -54,22 +55,24 @@ void process_packet() {
 	now = monotonic_now();
 	ses.last_packet_time_keepalive_recv = now;
 
-	/* These packets we can receive at any time */
-	switch(type) {
 
-		case SSH_MSG_IGNORE:
-			goto out;
-		case SSH_MSG_DEBUG:
-			goto out;
+	if (type == SSH_MSG_DISCONNECT) {
+		/* Allowed at any time */
+		dropbear_close("Disconnect received");
+	}
 
-		case SSH_MSG_UNIMPLEMENTED:
-			/* debugging XXX */
-			TRACE(("SSH_MSG_UNIMPLEMENTED"))
-			goto out;
-			
-		case SSH_MSG_DISCONNECT:
-			/* TODO cleanup? */
-			dropbear_close("Disconnect received");
+	/* These packets may be received at any time,
+	   except during first kex with strict kex */
+	if (!first_strict_kex) {
+		switch(type) {
+			case SSH_MSG_IGNORE:
+				goto out;
+			case SSH_MSG_DEBUG:
+				goto out;
+			case SSH_MSG_UNIMPLEMENTED:
+				TRACE(("SSH_MSG_UNIMPLEMENTED"))
+				goto out;
+		}
 	}
 
 	/* Ignore these packet types so that keepalives don't interfere with
@@ -98,7 +101,8 @@ void process_packet() {
 			if (type >= 1 && type <= 49
 				&& type != SSH_MSG_SERVICE_REQUEST
 				&& type != SSH_MSG_SERVICE_ACCEPT
-				&& type != SSH_MSG_KEXINIT)
+				&& type != SSH_MSG_KEXINIT
+				&& !first_strict_kex)
 			{
 				TRACE(("unknown allowed packet during kexinit"))
 				recv_unimplemented();
diff --git a/ssh.h b/ssh.h
index 1b4fec65f..ef3efdca0 100644
--- a/ssh.h
+++ b/ssh.h
@@ -100,6 +100,10 @@
 #define SSH_EXT_INFO_C "ext-info-c"
 #define SSH_SERVER_SIG_ALGS "server-sig-algs"
 
+/* OpenSSH strict KEX feature */
+#define SSH_STRICT_KEX_S "kex-strict-s-v00@openssh.com"
+#define SSH_STRICT_KEX_C "kex-strict-c-v00@openssh.com"
+
 /* service types */
 #define SSH_SERVICE_USERAUTH "ssh-userauth"
 #define SSH_SERVICE_USERAUTH_LEN 12
diff --git a/svr-session.c b/svr-session.c
index 769f0731d..a538e2c5c 100644
--- a/svr-session.c
+++ b/svr-session.c
@@ -370,6 +370,9 @@ static void svr_algos_initialise(void) {
 			algo->usable = 0;
 		}
 #endif
+		if (strcmp(algo->name, SSH_STRICT_KEX_C) == 0) {
+			algo->usable = 0;
+		}
 	}
 }
 
